/* Copyright (C) 1997-2005, Antony L Hosking.
 * All rights reserved.  */
package Semant;

import java.util.*;
import java.io.PrintWriter;

import Absyn.MiniJava.Exp;
import Absyn.MiniJava.SwitchBlock;
import Absyn.MiniJava.Stm.Switch;

public class MiniJava extends Absyn.MiniJava.Visitor<Void, Type> {
	private static void usage() {
		throw new Error(
		"Usage: java Semant.MiniJava [ -main=class ] <source>.java");
	}

	public static void main(String[] argv) throws java.io.IOException {
		String mainClass = null;
		if (argv.length < 1)
			usage();
		if (argv.length > 1)
			for (String arg : argv) {
				if (arg.startsWith("-main="))
					mainClass = arg.substring(6);
				else if (arg.startsWith("-"))
					usage();
			}
		String src = argv[argv.length - 1];
		Parse.MiniJava parse =
			new Parse.MiniJava(new java.io.FileInputStream(src));
		Absyn.MiniJava.Unit unit;
		try {
			unit = parse.CompilationUnit();
		} catch (Parse.ParseException e) {
			System.err.println(e.getMessage());
			return;
		} catch (Parse.TokenMgrError e) {
			System.err.println(e.getMessage());
			return;
		}
		MiniJava semant = new MiniJava(mainClass);
		Type type = unit.accept(semant, null);
		PrintWriter out = new java.io.PrintWriter(System.out);
		Type.Print printer = new Type.Print(out);
		type.accept(printer);
		out.println();
		out.flush();
	}

	private final String mainClassName;
	public MiniJava(String mainClass) {
		mainClassName = mainClass;
	}

	/**
	 * The classes defined by this compilation unit.
	 */
	public final Type.RECORD<Type.CLASS> classEnv =
		new Type.RECORD<Type.CLASS>();
	{
		classEnv.map.put("String", Type.STRING);
		classEnv.map.put("System", Type.SYSTEM);
	}

	/**
	 * The variable environment.
	 */
	private final Symbol.Table<String, Type> varEnv =
		new Symbol.Table<String, Type>();

	public boolean anyErrors = false;

	private void error(Absyn.MiniJava loc, String msg) {
		anyErrors = true;
		System.err.println(msg + ": line " + loc.line() + ", column "
				+ loc.column());
	}

	private Type.CLASS currentClass;
	private Type.FUN currentMethod;
	
	private int closestScope = -1; // -1 means empty, 0 means switch, 1 means loop 
	
	private Absyn.MiniJava.Stm currentLoop = null;
	private SwitchBlock currentCase = null;
	private int currSwitchBreakCount = 0;

	private void coerce(Type from, Type to, Absyn.MiniJava loc) {
		if (!from.coerceTo(to))
			error(loc, "incompatible types: required " + to + ", found " + from);
	}

	public Type visit(Absyn.MiniJava.Unit unit, Void _) {
		Type.CLASS mainClass = null;

		/*
		 * 1st pass
		 */
		// add class declarations
		for (Absyn.MiniJava.Dec.Class d : unit.types)
			d.accept(this, _);
		if (anyErrors)
			return classEnv;

		/*
		 * 2nd pass
		 */
		for (Absyn.MiniJava.Dec.Class d : unit.types) {
			currentClass = classEnv.map.get(d.toString());
			if (d.toString().equals(mainClassName))
				mainClass = currentClass;
			// bind parents
			if (d.type != null) {
				Type.CLASS p = classEnv.map.get(d.type.toString());
				if (p == null)
					error(d.type, "cannot resolve parent class: " + d.type);
				else if (p.isFinal)
					error(d.type, "cannot inherit from final class: " + d.type);
				currentClass.parent = p;
			}
			// add member declarations
			for (Absyn.MiniJava.Dec member : d.members)
				member.accept(this, null);
		}
		if (anyErrors)
			return classEnv;

		/*
		 * 3rd pass
		 */
		for (Absyn.MiniJava.Dec.Class d : unit.types) {
			currentClass = classEnv.map.get(d.toString());
			// check for cycles in inheritance
			if (currentClass.isLoop()) {
				error(d, "cyclic inheritance involving " + d);
				continue;
			}
			// check overrides
			for (Absyn.MiniJava.Dec m : d.members) {
				if (!(m instanceof Absyn.MiniJava.Dec.Method))
					continue;
				Type.FUN old;
				currentMethod = currentClass.methods.map.get(m.toString());
				if (currentMethod != null) {
					old = null;
					if (currentClass.parent != null)
						old = currentClass.parent.instance.method(m.toString());
					if (old != null)
						error(m, "static method override for non-static " + m);

					old = null;
					if (currentClass.parent != null)
						old = currentClass.parent.method(m.toString());
					if (old != null && !currentMethod.coerceTo(old))
						error(m, "incompatible method override for " + m);
				}
				currentMethod =
					currentClass.instance.methods.map.get(m.toString());
				if (currentMethod != null) {
					old = null;
					if (currentClass.parent != null)
						old = currentClass.parent.method(m.toString());
					if (old != null)
						error(m, "non-static method override for static " + m);

					old = null;
					if (currentClass.parent != null)
						old = currentClass.parent.instance.method(m.toString());
					if (old != null && !currentMethod.coerceTo(old))
						error(m, "incompatible method override for " + m);
				}
			}
		}
		if (anyErrors)
			return classEnv;

		/*
		 * 4th pass
		 */
		for (Absyn.MiniJava.Dec.Class d : unit.types) {
			currentClass = classEnv.map.get(d.toString());
			for (Absyn.MiniJava.Dec m : d.members) {
				if (!(m instanceof Absyn.MiniJava.Dec.Method))
					continue;
				Absyn.MiniJava.Stm.Block body =
					((Absyn.MiniJava.Dec.Method) m).body;
				if (body == null)
					continue;
				varEnv.beginScope();
				currentMethod =
					currentClass.instance.methods.map.get(m.toString());
				if (currentMethod == null)
					currentMethod = currentClass.methods.map.get(m.toString());
				else {
					varEnv.put("this", currentClass.instance);
					if (currentClass.parent != null)
						varEnv.put("super", currentClass.parent.instance);
				}
				for (Map.Entry<String, Type> f :
					currentMethod.formals.map.entrySet())
					varEnv.put(f.getKey(), f.getValue());
				Type result = body.accept(this, null);
				if (!result.coerceTo(currentMethod.result))
					error(m,
					"missing or incompatible return statement for method");
				varEnv.endScope();
			}
		}
		if (anyErrors)
			return classEnv;

		if (mainClassName != null) {
			if (mainClass == null) {
				System.err.println("No main class: " + mainClassName);
				anyErrors = true;
			} else {
				Type.FUN m = mainClass.method("main");
				if (m == null) {
					System.err.println("No static main method in class: "
							+ mainClassName);
					anyErrors = true;
				} else {
					Type.FUN fun = new Type.FUN(Type.VOID);
					fun.formals.map.put("args",
							new Type.ARRAY(Type.STRING.instance));
					if (!m.coerceTo(fun)) {
						System.err
						.println("Incompatible main method in class: "
								+ mainClassName);
						anyErrors = true;
					}
				}
			}
		}
		return classEnv;
	}

	public Type visit(Absyn.MiniJava.Name n, Void _) {
		Type t;
		if ((t = classEnv.map.get(n.toString())) != null)
			return t;
		error(n, "cannot resolve name");
		return Type.VOID;
	}

	private EnumSet<Absyn.MiniJava.Modifier.Kind> modifiers;

	public Type visit(Absyn.MiniJava.Modifier m, Void _) {
		if (!modifiers.add(m.kind))
			error(m, "repeated modifier");
		return null;
	}

	public Type visit(Absyn.MiniJava.Dec.Class d, Void _) {
		Type.CLASS c = new Type.CLASS(d.toString());
		if (classEnv.map.put(d.toString(), c) != null)
			error(d, "duplicate class: " + d);
		return c;
	}

	public Type visit(Absyn.MiniJava.Dec.Field d, Void _) {
		modifiers = EnumSet.noneOf(Absyn.MiniJava.Modifier.Kind.class);
		for (Absyn.MiniJava.Modifier mod : d.modifiers)
			mod.accept(this, _);
		Type type = d.type.accept(this, _);
		if (modifiers.contains(Absyn.MiniJava.Modifier.Kind.NATIVE))
			error(d, "field cannot be native");
		if (modifiers.contains(Absyn.MiniJava.Modifier.Kind.STATIC)) {
			if (currentClass.fields.map.put(d.toString(), type) != null
					|| currentClass.instance.fields.map.get(d.toString()) != null)
				error(d, "duplicate field");
			return type;
		}
		if (currentClass.instance.fields.map.put(d.toString(), type) != null
				|| currentClass.fields.map.get(d.toString()) != null)
			error(d, "duplicate field");
		return type;
	}

	public Type visit(Absyn.MiniJava.Dec.Method d, Void _) {
		modifiers = EnumSet.noneOf(Absyn.MiniJava.Modifier.Kind.class);
		for (Absyn.MiniJava.Modifier mod : d.modifiers)
			mod.accept(this, _);
		Type result = d.type.accept(this, _);
		if (modifiers.contains(Absyn.MiniJava.Modifier.Kind.NATIVE)) {
			if (!modifiers.contains(Absyn.MiniJava.Modifier.Kind.STATIC))
				error(d, "native method must be static");
			if (d.body != null)
				error(d, "native method must not have a body");
			currentMethod = new Type.FUN(result, currentClass.name + "_" + d);
		} else {
			if (d.body == null)
				error(d, "missing method body");
			currentMethod = new Type.FUN(result);
		}
		for (Absyn.MiniJava.Dec.Formal f : d.formals)
			f.accept(this, _);
		if (modifiers.contains(Absyn.MiniJava.Modifier.Kind.STATIC)) {
			if (currentClass.methods.map.put(d.toString(), currentMethod) != null
					|| currentClass.instance.methods.map.get(d.toString()) != null)
				error(d, "duplicate method");
			return currentMethod;
		}
		if (currentClass.instance.methods.map.put(d.toString(), currentMethod) != null
				|| currentClass.methods.map.get(d.toString()) != null)
			error(d, "duplicate method");
		return currentMethod;
	}

	public Type visit(Absyn.MiniJava.Dec.Formal d, Void _) {
		Type t = d.type.accept(this, _);
		if (currentMethod.formals.map.put(d.toString(), t) != null)
			error(d, "duplicate parameter");
		return t;
	}

	public Type visit(Absyn.MiniJava.Dec.Var d, Void _) {
		Type type = d.type.accept(this, _);
		if (d.init != null) {
			Type init = d.init.accept(this, _);
			coerce(init, type, d.init);
		}
		if (varEnv.put(d.toString(), type) != null)
			error(d, d + " is already defined in method");
		return Type.VOID;
	}

	public Type visit(Absyn.MiniJava.Type.Array t, Void _) {
		return new Type.ARRAY(t.element.accept(this, _));
	}

	public Type visit(Absyn.MiniJava.Type.Primitive t, Void _) {
		switch (t.kind) {
		case BOOLEAN:
			return Type.BOOLEAN;
		case INT:
			return Type.INT;
		case VOID:
			return Type.VOID;
		}
		throw new Error();
	}

	public Type visit(Absyn.MiniJava.Type.Class c, Void _) {
		Type.CLASS t;
		if ((t = classEnv.map.get(c.toString())) != null)
			return t.instance;
		error(c, "cannot resolve name: " + c);
		return Type.VOID;
	}

	public Type visit(Absyn.MiniJava.Stm.Block s, Void _) {
		Type type = Type.VOID;
		varEnv.beginScope();
		for (Absyn.MiniJava.Stm stm : s.stms)
			type = stm.accept(this, _);
		varEnv.endScope();
		return type;
	}

	public Type visit(Absyn.MiniJava.Stm.Break s, Void _) {
		if(closestScope==0){
			if(currentCase!=null && currSwitchBreakCount == 0){
				currSwitchBreakCount++;
			}
			else{
				error(s, "break statement outside Case Statement");
			}
		}
		else if(closestScope==1){
			if(currentLoop==null)
				error(s, "break statement outside loop or Switch Statement");
		}
		else{
			error(s, "break statement outside loop or Switch Statement");
		}
		return Type.VOID;
	}

	public Type visit(Absyn.MiniJava.Stm.Continue s, Void _) {
		if (currentLoop == null)
			error(s, "continue statement outside loop");
		return Type.VOID;
	}

	public Type visit(Absyn.MiniJava.Stm.Dec s, Void _) {
		return s.dec.accept(this, _);
	}

	public Type visit(Absyn.MiniJava.Stm.DoWhile s, Void _) {
		Absyn.MiniJava.Stm prevLoop = currentLoop;
		int prevClosestScope = closestScope;
		closestScope = 1;
		currentLoop = s;
		s.body.accept(this, _);
		currentLoop = prevLoop;
		closestScope = prevClosestScope;
		coerce(s.test.accept(this, _), Type.BOOLEAN, s.test);
		return Type.VOID;
	}

	public Type visit(Absyn.MiniJava.Stm.Exp s, Void _) {
		if (s.exp == null)
			return Type.VOID;
		if (!(s.exp instanceof Absyn.MiniJava.Exp.Assign
				|| s.exp instanceof Absyn.MiniJava.Exp.Call || s.exp instanceof Absyn.MiniJava.Exp.NewInstance))
			error(s.exp, "not a statement");
		s.exp.accept(this, _);
		return Type.VOID;
	}

	public Type visit(Absyn.MiniJava.Stm.For s, Void _) {
		varEnv.beginScope();
		
		if (s.init != null)
			for (Absyn.MiniJava.Stm v : s.init)
				v.accept(this, _);
		if (s.test != null)
			coerce(s.test.accept(this, _), Type.BOOLEAN, s.test);
		if (s.update != null)
			for (Absyn.MiniJava.Stm e : s.update)
				e.accept(this, _);
		Absyn.MiniJava.Stm prevLoop = currentLoop;
		currentLoop = s;
		int prevClosestScope = closestScope;
		closestScope = 1;
		s.body.accept(this, _);
		currentLoop = prevLoop;
		closestScope = prevClosestScope;
		varEnv.endScope();
		return Type.VOID;
	}

	public Type visit(Absyn.MiniJava.Stm.If s, Void _) {
		coerce(s.test.accept(this, _), Type.BOOLEAN, s.test);
		Type thenType = s.thenStm.accept(this, _);
		Type elseType =
			(s.elseStm != null) ? s.elseStm.accept(this, _) : Type.VOID;
			if (thenType.coerceTo(currentMethod.result)
					&& elseType.coerceTo(currentMethod.result))
				return currentMethod.result;
			else
				return Type.VOID;
	}

	public Type visit(Absyn.MiniJava.Stm.Return s, Void _) {
		Type result = Type.VOID;
		if (s.exp != null)
			result = s.exp.accept(this, _);
		if (!result.coerceTo(currentMethod.result))
			error(s, "incompatible return value");
		return result;
	}

	public Type visit(Absyn.MiniJava.Stm.While s, Void _) {
		coerce(s.test.accept(this, _), Type.BOOLEAN, s.test);
		Absyn.MiniJava.Stm prevLoop = currentLoop;
		int prevClosestScope = closestScope;
		closestScope = 1;
		currentLoop = s;
		s.body.accept(this, _);
		currentLoop = prevLoop;
		closestScope = prevClosestScope;
		return Type.VOID;
	}

	public Type visit(Absyn.MiniJava.Exp.ArrayAccess e, Void _) {
		coerce(e.index.accept(this, _), Type.INT, e.index);
		Type t = e.exp.accept(this, _);
		if (!(t instanceof Type.ARRAY)) {
			error(e, "incompatible types: array required, but " + t + " found");
			return e.type = Type.VOID;
		}
		return e.type = ((Type.ARRAY) t).element;
	}

	public Type visit(Absyn.MiniJava.Exp.Assign e, Void _) {
		Type left, right = e.right.accept(this, _);
		if (e.left instanceof Absyn.MiniJava.Exp.Id) {
			Absyn.MiniJava.Exp.Id n = (Absyn.MiniJava.Exp.Id) e.left;
			left = varEnv.get(n.toString());
			if (left == null) {
				error(n, "cannot resolve symbol: " + n);
				left = Type.VOID;
			}
		} else {
			left = e.left.accept(this, _);
			if (!(e.left instanceof Absyn.MiniJava.Exp.FieldAccess || e.left instanceof Absyn.MiniJava.Exp.ArrayAccess))
				error(e.left, "unexpected type: required variable, found value");
		}
		coerce(right, left, e);
		return e.type = left;
	}

	public Type visit(Absyn.MiniJava.Exp.Call e, Void _) {
		Type t = e.exp.accept(this, _);
		Type.FUN f = t.method(e.selector.toString());
		if (f == null && t instanceof Type.INSTANCE) {
			t = ((Type.INSTANCE)t).instanceOf;
			f = t.method(e.selector.toString());
		}
		if (f == null) {
			error(e.selector, "cannot resolve symbol: " + e.selector);
			return e.type = Type.VOID;
		}
		Iterator<Type> formals = f.formals.map.values().iterator();
		Iterator<Absyn.MiniJava.Exp> actuals = e.actuals.iterator();
		while (formals.hasNext() && actuals.hasNext()) {
			Type formal = formals.next();
			Absyn.MiniJava.Exp actual = actuals.next();
			coerce(actual.accept(this, _), formal, actual);
		}
		if (formals.hasNext() || actuals.hasNext())
			error(e, "mismatch in number of arguments");
		return e.type = f.result;
	}

	public Type visit(Absyn.MiniJava.Exp.Id e, Void _) {
		Type t;
		if ((t = varEnv.get(e.toString())) != null)
			return e.type = t;
		if ((t = classEnv.map.get(e.toString())) != null)
			return e.type = t;
		error(e, "cannot resolve symbol: " + e);
		return e.type = Type.VOID;
	}

	public Type visit(Absyn.MiniJava.Exp.If e, Void _) {
		coerce(e.test.accept(this, _), Type.BOOLEAN, e.test);
		Type thenType = e.thenExp.accept(this, _);
		Type elseType = e.elseExp.accept(this, _);
		
		if(e.thenExp.isConstant && e.elseExp.isConstant)
			e.isConstant = true;
		
		if (thenType.coerceTo(elseType))
			return e.type = elseType;
		if (elseType.coerceTo(thenType))
			return e.type = thenType;
		error(e, "incompatible types");
		return e.type = Type.VOID;
	}

	public Type visit(Absyn.MiniJava.Exp.Infix e, Void _) {
		Type l = e.left.accept(this, _);
		Type r = e.right.accept(this, _);
		
		if (e.left.isConstant && e.right.isConstant) {
			e.isConstant = true;
			switch (e.op){
			case ADD: 
				e.value = e.left.value + e.right.value;
				break;
			case SUB: 
				e.value = e.left.value - e.right.value;
				break;
			case MUL: 
				e.value = e.left.value * e.right.value;
				break;
			case DIV: 
				e.value = e.left.value / e.right.value;
				break;
			default: 
				e.isConstant = false;
			}
		}
		
		switch (e.op) {
		case OR:
		case AND:
			if (!(l.coerceTo(Type.BOOLEAN) && r.coerceTo(Type.BOOLEAN)))
				error(e, "operator " + e.op + " cannot be applied to " + l
						+ "," + r);
			return e.type = Type.BOOLEAN;
		case EQ:
		case NE:
			if (!((l.coerceTo(r) || r.coerceTo(l)))
					|| (l.coerceTo(Type.VOID) || r.coerceTo(Type.VOID)))
				error(e, "operator " + e.op + " cannot be applied to " + l
						+ "," + r);
			return e.type = Type.BOOLEAN;
		case GT:
		case GE:
		case LT:
		case LE:
			if (!(l.coerceTo(Type.INT) && r.coerceTo(Type.INT)))
				error(e, "operator " + e.op + " cannot be applied to " + l
						+ "," + r);
			return e.type = Type.BOOLEAN;
		case ADD:
		case SUB:
		case MUL:
		case DIV:
			if (!(l.coerceTo(Type.INT) && r.coerceTo(Type.INT)))
				error(e, "operator " + e.op + " cannot be applied to " + l
						+ "," + r);
			return e.type = Type.INT;
		default:
			throw new Error();
		}
	}

	public Type visit(Absyn.MiniJava.Exp.FieldAccess e, Void _) {
		Type t = e.exp.accept(this, _);
		Type f = t.field(e.selector.toString());
		if (f == null && t instanceof Type.INSTANCE) {
			t = ((Type.INSTANCE)t).instanceOf;
			f = t.field(e.selector.toString());
		}
		if (f == null) {
			error(e.selector, "cannot resolve field: " + e.selector);
			return e.type = Type.VOID;
		}
		return e.type = f;
	}

	public Type visit(Absyn.MiniJava.Exp.Literal e, Void _) {
		switch (e.kind) {
		case TRUE:
		case FALSE:
			return e.type = Type.BOOLEAN;
		case STRING:
			return e.type = Type.STRING.instance;
		case CHAR:
		case DEC:
		case HEX:
		case OCT:
			return e.type = Type.INT;
		case NULL:
			return e.type = Type.NIL;
		}
		throw new Error();
	}

	public Type visit(Absyn.MiniJava.Exp.NewArray e, Void _) {
		Type t = e.element.accept(this, _);
		for (Absyn.MiniJava.Exp exp : e.dims) {
			if (exp != null)
				coerce(exp.accept(this, _), Type.INT, exp);
			t = new Type.ARRAY(t);
		}
		return e.type = t;
	}

	public Type visit(Absyn.MiniJava.Exp.NewInstance e, Void _) {
		Type.CLASS c = classEnv.map.get(e.classType.toString());
		if (c == null) {
			error(e.classType, "cannot resolve name: " + e.classType);
			return e.type = Type.VOID;
		}
		return e.type = c.instance;
	}

	public Type visit(Absyn.MiniJava.Exp.Prefix e, Void _) {
		Type exp = e.exp.accept(this, _);
		switch (e.op) {
		case NOT:
			if (!exp.coerceTo(Type.BOOLEAN))
				error(e, "operator " + e.op + " cannot be applied to " + exp);
			return e.type = Type.BOOLEAN;
		case NEG:
			if (!exp.coerceTo(Type.INT))
				error(e, "operator " + e.op + " cannot be applied to " + exp);
			if (e.exp.isConstant)
			{
				e.isConstant = true;
				e.value = -e.exp.value;
			}
			return e.type = Type.INT;
		default:
			throw new Error();
		}
	}

	public Type visit(Absyn.MiniJava.Exp.Super e, Void _) {
		Type t;
		if ((t = varEnv.get("this")) == null) {
			error(e, "non=static variable super referenced from static context");
			return e.type = Type.VOID;
		}
		if ((t = varEnv.get("super")) == null) {
			error(e, "cannot resolve symbol: super");
			return e.type = Type.VOID;
		}
		return e.type = t;
	}

	public Type visit(Absyn.MiniJava.Exp.This e, Void _) {
		Type t;
		if ((t = varEnv.get("this")) == null) {
			error(e, "non-static variable this referenced from static context");
			return e.type = Type.VOID;
		}
		return e.type = t;
	}

	public Type visit(Switch e, Void _) {
		
		e.case_expression.accept(this, _);
		if (!e.case_expression.type.coerceTo(Type.INT))
			error(e, "type of switch expression is not valid");
		
		e.switch_block.accept(this, _);
		
		return Type.VOID;
	}

	public Type visit(SwitchBlock switchBlock, Void _) {
		int currentIndex = 0;
		List<Absyn.MiniJava.Stm> stms;

		int[] tempValues = new int[switchBlock.expressions.size()];

		varEnv.beginScope();
		
		for (Exp exp : switchBlock.expressions) {
			if (currentIndex != switchBlock.index_of_default) {
				exp.accept(this, _);

				// check if the expression is constant
				if (!exp.isConstant)
					error(exp, "type of case expression is not valid");

				// check for duplicate cases
				tempValues[currentIndex] = exp.value;
				for (int innerIter = currentIndex - 1; innerIter >= 0; innerIter--)
					if (tempValues[innerIter] == tempValues[currentIndex] && (innerIter != switchBlock.index_of_default))
						error(exp, "Duplicated Case Expression");
			}
			
			stms = switchBlock.stmlists.get(currentIndex);
			if (!stms.isEmpty()) {
				int prevSwitchBreakCount = currSwitchBreakCount;
				currSwitchBreakCount = 0;
				int prevClosestScope = closestScope;
				closestScope = 0;
				SwitchBlock prevSwitchCase = currentCase;
				currentCase = switchBlock;
				for(Absyn.MiniJava.Stm st:stms)
					st.accept(this, _);

				currentCase = prevSwitchCase;
				closestScope = prevClosestScope;
				currSwitchBreakCount = prevSwitchBreakCount;
			}

			currentIndex++;
		}
		
		varEnv.endScope();

		return Type.VOID;
	}
	
}
